Skip to content

Revert if a call fails in an ERC2771Forwarder atomic batch#6391

Merged
ernestognw merged 6 commits intoOpenZeppelin:masterfrom
Amxx:fix/ERC2771Forwarder/revert-if-call-in-an-atomic-batch-fails
Mar 5, 2026
Merged

Revert if a call fails in an ERC2771Forwarder atomic batch#6391
ernestognw merged 6 commits intoOpenZeppelin:masterfrom
Amxx:fix/ERC2771Forwarder/revert-if-call-in-an-atomic-batch-fails

Conversation

@Amxx
Copy link
Collaborator

@Amxx Amxx commented Mar 4, 2026

Fixes H-04

PR Checklist

  • Tests
  • Documentation
  • Changeset entry (run npx changeset add)

@changeset-bot
Copy link

changeset-bot bot commented Mar 4, 2026

🦋 Changeset detected

Latest commit: 9e1b991

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Amxx Amxx requested a review from a team as a code owner March 4, 2026 09:45
@Amxx Amxx added the security Pull requests that address a security vulnerability label Mar 4, 2026
@Amxx Amxx requested a review from ernestognw March 4, 2026 09:46
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Walkthrough

The changes introduce atomic batch execution semantics to ERC2771Forwarder. A new error (ERC2771ForwarderFailureInAtomicBatch) is added to signal failures in atomic batches. The executeBatch function now reverts the entire batch when any request fails and the batch is atomic (indicated by refundReceiver being address(0)). Non-atomic batch behavior remains unchanged. A corresponding test case verifies that the batch reverts when a single request fails, confirming the atomic behavior.

Suggested labels

tests

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is related to the changeset, referencing a specific audit issue (H-04) being fixed and documenting completion of tests and changeset requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding revert behavior when calls fail in ERC2771Forwarder atomic batches, which aligns with the core changeset across all three modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

@Amxx
Copy link
Collaborator Author

Amxx commented Mar 4, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/metatx/ERC2771Forwarder.test.js (1)

231-243: Strengthen this atomicity test with post-revert state invariants.

This currently validates the revert reason, but not the “no side effects” guarantee (nonces/ETH state) after failure.

Suggested test hardening
       it('atomic batch with reverting request reverts the whole batch', async function () {
+        const noncesBefore = await Promise.all(
+          this.requests.map(request => this.forwarder.nonces(request.from)),
+        );
+        const forwarderBalanceBefore = await ethers.provider.getBalance(await this.forwarder.getAddress());
+
         // Add extra reverting request
         await this.forgeRequest(
           { value: 10n, data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason') },
           this.accounts[requestCount],
         ).then(extraRequest => this.requests.push(extraRequest));
         // recompute total value with the extra request
         this.value = requestsValue(this.requests);

         await expect(
           this.forwarder.executeBatch(this.requests, ethers.ZeroAddress, { value: this.value }),
         ).to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderFailureInAtomicBatch');
+
+        expect(await ethers.provider.getBalance(await this.forwarder.getAddress())).to.equal(forwarderBalanceBefore);
+        for (const [i, request] of this.requests.slice(0, requestCount).entries()) {
+          expect(await this.forwarder.nonces(request.from)).to.equal(noncesBefore[i]);
+        }
       });

Based on learnings: in revert-semantics tests, also assert no ETH balance changes occurred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/metatx/ERC2771Forwarder.test.js` around lines 231 - 243, Capture
pre-revert state (store balances and nonces) for the involved parties before
adding the reverting request — e.g., record ethers.provider.getBalance for the
sender(s), the forwarder contract, and receiver, and record per-sender nonce via
this.forwarder.getNonce(sender) or equivalent; then run the existing
expect(...).to.be.revertedWithCustomError(this.forwarder,
'ERC2771ForwarderFailureInAtomicBatch'); finally assert the post-revert balances
and nonces equal the pre-revert values (and that this.requests / this.value
didn’t change) to enforce no ETH or nonce side effects after executeBatch with
the reverting mockFunctionRevertsNoReason.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/some-dolls-shine.md:
- Line 5: Update the changeset sentence for ERC2771Forwarder to use correct
plural form: replace "Revert the entire atomic batch if one of the call fails."
with "Revert the entire atomic batch if one of the calls fails." so the
subject/verb agreement is correct; locate the string mentioning
`ERC2771Forwarder` in the changeset and make this single-word change.

---

Nitpick comments:
In `@test/metatx/ERC2771Forwarder.test.js`:
- Around line 231-243: Capture pre-revert state (store balances and nonces) for
the involved parties before adding the reverting request — e.g., record
ethers.provider.getBalance for the sender(s), the forwarder contract, and
receiver, and record per-sender nonce via this.forwarder.getNonce(sender) or
equivalent; then run the existing
expect(...).to.be.revertedWithCustomError(this.forwarder,
'ERC2771ForwarderFailureInAtomicBatch'); finally assert the post-revert balances
and nonces equal the pre-revert values (and that this.requests / this.value
didn’t change) to enforce no ETH or nonce side effects after executeBatch with
the reverting mockFunctionRevertsNoReason.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4fe53547-879e-42c3-b4ae-728610c7b97a

📥 Commits

Reviewing files that changed from the base of the PR and between 95a983a and b137f20.

📒 Files selected for processing (3)
  • .changeset/some-dolls-shine.md
  • contracts/metatx/ERC2771Forwarder.sol
  • test/metatx/ERC2771Forwarder.test.js

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/metatx/ERC2771Forwarder.test.js (1)

231-243: Strengthen this test with post-revert invariants (nonces + ETH).

Nice revert assertion. I’d also assert that signer nonces and forwarder ETH are unchanged after revert to directly lock in the audited regression behavior.

Suggested test hardening
 it('atomic batch with reverting request reverts the whole batch', async function () {
   // Add extra reverting request
   await this.forgeRequest(
     { value: 10n, data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason') },
     this.accounts[requestCount],
   ).then(extraRequest => this.requests.push(extraRequest));
   // recompute total value with the extra request
   this.value = requestsValue(this.requests);
+
+  const noncesBefore = await Promise.all(this.requests.map(request => this.forwarder.nonces(request.from)));
+  const forwarderBalanceBefore = await ethers.provider.getBalance(this.forwarder);

   await expect(
     this.forwarder.executeBatch(this.requests, ethers.ZeroAddress, { value: this.value }),
   ).to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderFailureInAtomicBatch');
+
+  for (const [i, request] of this.requests.entries()) {
+    expect(await this.forwarder.nonces(request.from)).to.equal(noncesBefore[i]);
+  }
+  expect(await ethers.provider.getBalance(this.forwarder)).to.equal(forwarderBalanceBefore);
 });

Based on learnings: in revert-semantics tests, verify no balance effects occurred (e.g., negative balance-change assertions) to ensure no unintended state/value changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/metatx/ERC2771Forwarder.test.js` around lines 231 - 243, Before calling
executeBatch, record the current signer nonces for the involved accounts and the
forwarder contract ETH balance (use the same accounts from this.accounts and the
forwarder instance this.forwarder); after the
expect(...revertedWithCustomError(...)) assertion, re-query those nonces and the
forwarder balance and assert they are unchanged to ensure no state or value
changes occurred; locate the setup around the atomic batch test that builds
this.requests (including the extra request using mockFunctionRevertsNoReason)
and the compute of this.value via requestsValue to add these pre- and
post-revert invariants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/metatx/ERC2771Forwarder.test.js`:
- Around line 231-243: Before calling executeBatch, record the current signer
nonces for the involved accounts and the forwarder contract ETH balance (use the
same accounts from this.accounts and the forwarder instance this.forwarder);
after the expect(...revertedWithCustomError(...)) assertion, re-query those
nonces and the forwarder balance and assert they are unchanged to ensure no
state or value changes occurred; locate the setup around the atomic batch test
that builds this.requests (including the extra request using
mockFunctionRevertsNoReason) and the compute of this.value via requestsValue to
add these pre- and post-revert invariants.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f65055df-abf7-421c-8cf1-6fcbbf4a4cb5

📥 Commits

Reviewing files that changed from the base of the PR and between 95a983a and b137f20.

📒 Files selected for processing (3)
  • .changeset/some-dolls-shine.md
  • contracts/metatx/ERC2771Forwarder.sol
  • test/metatx/ERC2771Forwarder.test.js

Amxx and others added 2 commits March 4, 2026 10:56
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@ernestognw ernestognw changed the title Revert if a call fails in an atomic batch Revert if a call fails in an ERC2771Forwarder atomic batch Mar 5, 2026
@ernestognw ernestognw merged commit 77e5684 into OpenZeppelin:master Mar 5, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

security Pull requests that address a security vulnerability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants